INFORME HITO 3

Predicción de calificación otorgada a cervezas a partir del texto ingresado por los usuarios.

Grupo 9

Sebastián Arriola, David Rojas, Sandra Romero, Luis Torres

1.- Introducción

El consumo de cerveza en Chile crece año a año. Desde 2007 a la fecha, pasamos de consumir 37 litros a 47, rompiendo récords de importación. El último recuento del año pasado habla de 206 millones de litros, la mayoría desde Estados Unidos, México y Alemania. Pero eso no es todo, ya que la industria nacional también saca réditos de ese auge.

El último encuentro de la Asociación de Productores de Cerveza de Chile (Acechi) hace un mes reveló un dato no menor: la industria de producción nacional es la catorceava en importancia para Chile, representando el 0,1% del PIB chileno.

A su vez, el consumidor, cada vez más informado y exigente, ha desafiado a los productores a desarrollar productos variados, que responden a diversas ocasiones de consumo. Por ello la industria cervecera debe evolucionar en calidad, autenticidad y oferta, adecuándose a los requerimientos de nuevos públicos.

Es por esto que poseer un set de datos con información sobre calificaciones de cervezas realizado por usuarios (personas naturales) podría resultar de gran valor en términos financieros y de mercado, por ejemplo para una eventual empresa ligada a la industria cervecera, pudiendo así definir la fabricación de un producto en función de los gustos explicitados por los evaluadores o también acorde a la valoración que se da a aspectos como el sabor o apariencia.

1.1 Datos

Los datos recibidos se encuentran en un archivo json con 50.000 entradas, en donde cada entrada corresponde a un review de una cerveza en particular realizado por un usuario. Como pre-procesamiento, a través de un programa en lenguaje python, se toma este archivo json y se convierte a un archivo de texto plano con filas y columnas, el cual puede ser exportado a R studio y a python para su análisis.

1.2 Características más relevantes de los datos

Una vez convertido el set de datos a un archivo con filas y columnas, para cada fila que corresponde a un review se pueden identificar 3 grupos de información: Por un lado se tiene información referente a usuarios, los cuales se caracterizan con un nombre de perfil, y opcionalmente pueden proveer su género y fecha de nacimiento. Con respecto a la información de cada cerveza, para estas se indica su nombre, estilo, ABV (The alcoholic content by volume, contenido de alcohol por unidad de volumen), un ID de la cerveza y además un ID de la cervecera. Finalmente se tiene información con respecto al review propiamente tal. El usuario entrega distintas puntuaciones, sobre apariencia, aroma, gusto sabor, y una general, también existe una fecha y hora del review, y además un texto descriptivo.

1.3 Hipótesis del trabajo

Se espera poder estimar la percepción que tienen los usuario sobre las cervezas determinado por lo que escriben en el texto descriptivo. En primer lugar, se busca identificar una categoría en función de lo que escribe el usuario para determinar si su percepción del producto es mala, buena, o intermedia. Además, se intentará predecir el valor numérico del puntaje asignado a partir del comentario provisto. Por esto pues, para este análisis en particular, se hará uso de dos columnas del set de datos: El puntaje general otorgado por los usuarios a la cerveza y el texto ingresado. Se procede entonces a hacer una pequeña exploración de las columnas a utilizar.

2.- Exploración de los datos

Se observa en primer lugar el comportamiento de los calificaciones otorgadas por las usuarios.

In [22]:
from IPython.display import Image
Image("punt.png")
Out[22]:

Se puede ver que la tendencia de los datos tiene mucha similitud a una distribución normal, y por lo tanto está desbalanceada. Es por esto que se decide crear categorías para las puntuaciones y luego balancear las clases haciendo sub-sampling. Junto a esto, también se realizarán ensayos considerando el set completo de datos, pero usando pesos para las clases. Para el ensayo con sum-sampling, se asume que si un usuario otorga una nota entre 0 y 2.5, su percepción de la cerveza es mala; si la nota está en 2.5 a 3.5 su percepción es intermedia, y superior a 3.5, su percepción de la cerveza es buena.

In [2]:
import pandas as pd 
import os 
from datetime import *
import numpy as np
import matplotlib.pyplot as plt

data = pd.read_csv('beer2.dat', sep=" ", header=None, names=["ABV","beerId","brewerId","name","style","appearance","aroma","overall","palate","taste","datetime","timeUnix","profileName","ageInSeconds","birthdayRaw","birthdayUnix","gende","text"])

aux_string=[]
for i in range(0,len(data["text"])):
    prelim_string ="{0}".format(data["text"][i])
    prelim_string=' '.join(prelim_string.split())
    aux_string.append(prelim_string)

data["review"]=aux_string

def points_to_class(points):
    if points>=0 and points<=2.5:
        return 0
    elif points>2.5 and points<=3.5:
        return 1
    else:
        return 2
    
rating = data["overall"].apply(points_to_class)

rating.value_counts()
Out[2]:
2    33612
1    13153
0     3235
Name: overall, dtype: int64

Se puede ver que, además, se analiza la distribución de datos en las clases asignadas para generar los respectivos pesos.

In [3]:
class_weights = {0: 10,
                1: 3,
                2: 1}

Por otro lado, se analiza en el texto la presencia de palabras más comunes. Para eso se utiliza el siguiente código

In [25]:
#Grafico de palabras recurrentes. parte de este codigo se tomo de https://towardsdatascience.com/
import seaborn as sns 
import matplotlib.pyplot as plt
from nltk.tokenize.treebank import TreebankWordDetokenizer
from nltk.corpus import stopwords
from nltk import word_tokenize

sns.set(style="whitegrid")
stopwords = set(stopwords.words('english'))
detokenizer = TreebankWordDetokenizer()

def clean_description(desc):
    desc = word_tokenize(desc.lower())
    desc = [token for token in desc if token not in stopwords and token.isalpha()]
    return detokenizer.detokenize(desc)

data["cleaned_text"] = data["review"].apply(clean_description)

word_occurrence = data["cleaned_text"].str.split(expand=True).stack().value_counts()

total_words = sum(word_occurrence)
top_words = word_occurrence[:30]/total_words
ax = sns.barplot(x = top_words.values, y = top_words.index)

# Setting title 
ax.set_title("% de ocurrencia de palabras más frecuentes")

plt.show()

Además, se analizan por completitud aspectos generales de los datos

In [26]:
data.describe()
Out[26]:
ABV beerId brewerId appearance aroma overall palate taste timeUnix
count 50000.000000 50000.00000 50000.000000 50000.0000 50000.000000 50000.00000 50000.000000 50000.000000 5.000000e+04
mean 7.400287 21884.03154 3035.736540 3.8984 3.871520 3.88871 3.852670 3.922250 1.232761e+09
std 2.317491 18877.19133 5115.236354 0.5898 0.682541 0.70174 0.666309 0.716645 7.193478e+07
min 0.100000 175.00000 1.000000 0.0000 1.000000 0.00000 1.000000 1.000000 9.262944e+08
25% 5.400000 5441.00000 395.000000 3.5000 3.500000 3.50000 3.500000 3.500000 1.189388e+09
50% 6.900000 17538.00000 1199.000000 4.0000 4.000000 4.00000 4.000000 4.000000 1.248142e+09
75% 9.400000 34146.00000 1382.250000 4.5000 4.500000 4.50000 4.500000 4.500000 1.291320e+09
max 57.700000 77213.00000 27797.000000 5.0000 5.000000 5.00000 5.000000 5.000000 1.326267e+09
In [27]:
data.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 50000 entries, 0 to 49999
Data columns (total 20 columns):
ABV             50000 non-null float64
beerId          50000 non-null int64
brewerId        50000 non-null int64
name            50000 non-null object
style           50000 non-null object
appearance      50000 non-null float64
aroma           50000 non-null float64
overall         50000 non-null float64
palate          50000 non-null float64
taste           50000 non-null float64
datetime        50000 non-null object
timeUnix        50000 non-null int64
profileName     49991 non-null object
ageInSeconds    50000 non-null object
birthdayRaw     50000 non-null object
birthdayUnix    50000 non-null object
gende           50000 non-null object
text            49983 non-null object
review          50000 non-null object
cleaned_text    50000 non-null object
dtypes: float64(6), int64(3), object(11)
memory usage: 9.3+ MB

Como se dijo anteriormente, se desea realizar un ensayo con y sin balanceo de las clases, para lo cual se utiliza el siguiente código de sub-sampling que equipara el número de elementos por clase al de menor cardinalidad:

In [4]:
# Este funcion se tomó de http://www.developintelligence.com/

from collections import Counter
 
def balance_classes(xs, ys):
    freqs = Counter(ys)
    max_allowable = freqs.most_common()[-1][1]
    num_added = {clss: 0 for clss in freqs.keys()}
    new_ys = []
    new_xs = []
    for i, y in enumerate(ys):
        if num_added[y] < max_allowable:
            new_ys.append(y)
            new_xs.append(xs[i])
            num_added[y] += 1
    return new_xs, new_ys

aux_string_sub, rating_sub = balance_classes(aux_string, rating)

Puesto que se han definido clases asociadas a los puntajes, el primer predictor a implementar será un clasificador. Para entrenar tanto este clasificador como un regresor se usarán los textos provistos por los usuarios, los cuales por lo tanto deben ser vectorizados de alguna manera.

2.1 Vectorización mediante TF-IDF

se utiliza el método TF-IDF "Term Frequency Inverse Document Frequency ", el cual se apoya en la idea de que palabras muy comunes son menos relevantes y así normalizar el vector de palabras utilizando las frecuencias en los textos. Este método está implementado en scikit-learn

In [6]:
from sklearn.feature_extraction.text import TfidfVectorizer
 
vectorizer = TfidfVectorizer(ngram_range=(1,10))

vectors_sub = vectorizer.fit_transform(aux_string_sub)

vectors = vectorizer.fit_transform(aux_string)

Que además permite indicar un número de n-gramas para vectorizar los textos. Esto indica como se agrupan las palabras para indicar la presencia de frases. En este caso se usa el número 10 como límite superior, pues se estima que 10-gramas son capaces de representar las ideas de los usuarios de manera adecuada.

2.1.1 Clustering sobre la vectorización de palabras TF-IDF

Una vez realizada la vectorización de palabras, como parte del análisis explotario se hace un clustering sobre los vectores para identificar la presencia de caracteristicas similares entre reviews. Para ello se utiliza K-means con distinto número de clusters, y calculando el error, se grafican los resultados con tal de ver si es factible seguir la metodología "del codo".

In [ ]:
from sklearn.cluster import KMeans
import matplotlib.pyplot as plt


sse = {}
for k in range(2, 10):
    kmeans = KMeans(n_clusters=k, max_iter=1000).fit(vectors)
    sse[k] = kmeans.inertia_ 
    
plt.figure()
plt.plot(list(sse.keys()), list(sse.values()))
plt.xlabel("Numero de clusters")
plt.ylabel("SSE")
plt.show()
In [16]:
from IPython.display import Image
Image("clustering.png")
Out[16]:

Si bien este clustering no aporta mucha información debido al comportamiento de la curva, se debe ver una inflexión considerable en K=3, que se espera pueda tener correlación con la elección de tres clases distintas para categorizar la percepción de los usuarios. Se ve otra inflexión importante en K=8, pero se prescinde de ella debido a que dado que el rango numérico de los datos es entre 0 y 5, definir 8 clases parace un poco excesivo para la naturaleza de los datos.

3.- Clasificador

Como se comentó anteriormente, se realizarán ensayos utilizando el set de datos con y sin sub-sampling. Para el caso sin sub-sampling, se utilizarán pesos definidos para cada clase. A la hora de evaluar el desempeño de los distintos clasificadores, se recurre al método de cross-validation, dividiendo el set de datos en 10 grupos. Así, en cada iteración, se entrena con un 90% de los datos y se prueba con el 10% restante, para luego evaluar los promedios de una serie de indicadores: Precision, recall, F1-score y accuracy.

Por otro lado,se crea un nuevo set de datos de entrenamiento y testing con la misma proporción indicada en el párrafo anterior, con tal de poder visualizar una matriz de confusión sobre los resultados obtenidos:

In [7]:
from sklearn.model_selection import train_test_split

X_train_sub, X_test_sub, y_train_sub, y_test_sub = train_test_split(vectors_sub, rating_sub, test_size=0.1)

X_train, X_test, y_train, y_test = train_test_split(vectors, rating, test_size=0.1)

3.1 Clasificador usando SVM no lineal

El primer clasificador a probar corresponde a SVM no lineal. Primero se presentan los resultados obtenidos al usar el set de datos sub-sampleado, y posteriormente el set de datos completo. Para esto se utiliza el siguiente código, que luego muestra su desempeño con distintas métricas, y una matriz de confusión para un ensayo:

In [31]:
#CLASIFICADOR USANDO SVM sumsampled

from sklearn.svm import LinearSVC
from sklearn.svm import NuSVC
from sklearn.model_selection import cross_validate
from sklearn.metrics import accuracy_score
from sklearn.metrics import classification_report
######################################################
######################################################
# SVM NO LINEAL
######################################################
######################################################


classifier1_sub = NuSVC(gamma='auto')

scoring = ['precision_macro', 'recall_macro', 'accuracy', 'f1_macro']
cv_results_sub = cross_validate(classifier1_sub, vectors_sub, rating_sub, cv = 10, n_jobs=10,scoring = scoring, return_train_score= True)

print('Promedio Precision:', np.mean(cv_results_sub['test_precision_macro']))
print('Promedio Recall:', np.mean(cv_results_sub['test_recall_macro']))
print('Promedio F1-score:', np.mean(cv_results_sub['test_f1_macro']))
print('Promedio Accucary:', np.mean(cv_results_sub['test_accuracy']))


classifier1_sub.fit(X_train_sub, y_train_sub)

preds_sub = classifier1_sub.predict(X_test_sub)




from sklearn.metrics import confusion_matrix
cm1_sub = confusion_matrix(y_test_sub, preds_sub)
print(cm1_sub)
Promedio Precision: 0.5082575895266552
Promedio Recall: 0.45926435551478545
Promedio F1-score: 0.43581197040929903
Promedio Accucary: 0.45926435551478545
[[172  81  66]
 [ 50 123 147]
 [ 20  27 285]]

El desempeño del clasificador no muestra valores óptimos en las métricas. Su precisión marca aproximadamente un 50% de efectividad, y en la matriz de confusión se puede ver que comete muchos errores a la hora de clasificar, aún cuando los valores en las diagonales son altos.

In [39]:
#CLASIFICADOR USANDO SVM

classifier1 = NuSVC(gamma='auto',class_weight = class_weights,nu=0.1)

cv_results = cross_validate(classifier1, vectors, rating, cv = 10, n_jobs=10,scoring = scoring, return_train_score= True)

print('Promedio Precision:', np.mean(cv_results['test_precision_macro']))
print('Promedio Recall:', np.mean(cv_results['test_recall_macro']))
print('Promedio F1-score:', np.mean(cv_results['test_f1_macro']))
print('Promedio Accucary:', np.mean(cv_results['test_accuracy']))


classifier1.fit(X_train, y_train)

preds = classifier1.predict(X_test)




from sklearn.metrics import confusion_matrix
cm1 = confusion_matrix(y_test, preds)
print(cm1)
Promedio Precision: 0.49177756696598307
Promedio Recall: 0.5612395910089295
Promedio F1-score: 0.4553878138993731
Promedio Accucary: 0.5546394810629571
[[ 180   80   78]
 [ 236  487  583]
 [ 191  367 2798]]

Para el set de datos completo, los resultados son bastante similares en término de las métricas. No se observan cambios muy relevantes a la hora de usar este set de datos completo.

3.2 Clasificador usando árbol de decisión

El siguiente caso corresponde al uso de un clasificador con árbol de decisión. Análogamente al caso anterior, se estudian métricas para el uso del set de datos diezmado y posteriormente completo.

In [40]:
#CLASIFICADOR USANDO Árbol de Decisión subsampled

from sklearn.tree import DecisionTreeClassifier
classifier2_sub = DecisionTreeClassifier()
cv_results2_sub = cross_validate(classifier2_sub, vectors_sub, rating_sub, cv = 10, n_jobs=10, scoring = scoring, return_train_score= True)


print('Promedio Precision:', np.mean(cv_results2_sub['test_precision_macro']))
print('Promedio Recall:', np.mean(cv_results2_sub['test_recall_macro']))
print('Promedio F1-score:', np.mean(cv_results2_sub['test_f1_macro']))
print('Promedio Accucary:', np.mean(cv_results2_sub['test_accuracy']))
# train the classifier

classifier2_sub.fit(X_train_sub, y_train_sub)


preds2_sub = classifier2_sub.predict(X_test_sub)



from sklearn.metrics import confusion_matrix

cm2_sub = confusion_matrix(y_test_sub, preds2_sub)
print(cm2_sub)
Promedio Precision: 0.4426874437002457
Promedio Recall: 0.44104619755634544
Promedio F1-score: 0.4412520994618139
Promedio Accucary: 0.4410461975563454
[[169  97  53]
 [ 82 122 116]
 [ 54 116 162]]

Nuevamente los resultados no son muy prometedores. Las métricas muestran que los resultados son incluso peores que en el caso anterior. Lo mismo ocurre si se clasifica usando el set de datos completo, sin sub-sampling.

In [41]:
#CLASIFICADOR USANDO Árbol de Decisión

from sklearn.tree import DecisionTreeClassifier
classifier2 = DecisionTreeClassifier(class_weight = class_weights)
scoring2 = ['precision_macro', 'recall_macro', 'accuracy', 'f1_macro']
cv_results2 = cross_validate(classifier2, vectors, rating, cv = 10, n_jobs=10, scoring = scoring, return_train_score= True)


print('Promedio Precision:', np.mean(cv_results2['test_precision_macro']))
print('Promedio Recall:', np.mean(cv_results2['test_recall_macro']))
print('Promedio F1-score:', np.mean(cv_results2['test_f1_macro']))
print('Promedio Accucary:', np.mean(cv_results2['test_accuracy']))
# train the classifier

classifier2.fit(X_train, y_train)


preds2 = classifier2.predict(X_test)



from sklearn.metrics import confusion_matrix

cm2 = confusion_matrix(y_test, preds2)
print(cm2)
Promedio Precision: 0.4300483842284323
Promedio Recall: 0.4391303947798818
Promedio F1-score: 0.4326527814644706
Promedio Accucary: 0.5832197563360437
[[  84  130  124]
 [ 120  503  683]
 [ 129  854 2373]]

3.3 Perceptrón multi-capa

Finalmente, y con tal de poder comparar, se utiliza un perceptrón multi-capa. Este clasificador, en su implementación de scikit-learn, no permite indicar el peso para las clases, así que solamente se usará el set de datos con sub-sampling.

In [42]:
#CLASIFICADOR USANDO REDES NEURONALES

from sklearn.neural_network import MLPClassifier


# initialise the SVM classifier
classifier3_sub = MLPClassifier(hidden_layer_sizes=(7,7), random_state=1)
 
cv_results3_sub = cross_validate(classifier3_sub, vectors_sub, rating_sub, cv = 10, n_jobs=10, scoring = scoring, return_train_score= True)

print('Promedio Precision:', np.mean(cv_results3_sub['test_precision_macro']))
print('Promedio Recall:', np.mean(cv_results3_sub['test_recall_macro']))
print('Promedio F1-score:', np.mean(cv_results3_sub['test_f1_macro']))
print('Promedio Accucary:', np.mean(cv_results3_sub['test_accuracy']))
# train the classifier

classifier3_sub.fit(X_train_sub, y_train_sub)


preds3_sub = classifier3_sub.predict(X_test_sub)



from sklearn.metrics import confusion_matrix

cm3_sub = confusion_matrix(y_test_sub, preds3_sub)
print(cm3_sub)
Promedio Precision: 0.6152843511960597
Promedio Recall: 0.5034377428684275
Promedio F1-score: 0.48099885363257433
Promedio Accucary: 0.5034377428684275
[[173 146   0]
 [ 31 260  29]
 [ 16 153 163]]

Para este caso, se puede ver que la métrica de precisión mejora hasta un 61% aproximadamente, y en la matriz de confusión se puede ver que los valores mayores están sobre la diagonal, cometiendo errores en un porcentaje importante de los casos por asignar una clase vecina en lugar de la correcta.

4.- Regresión

Luego del análisis de clasificación, se implementará una serie de regresores para intentar estimar la puntuación asignada por el usuario en función del texto provisto. Para esto, puesto que ya no existen clases, se utilizará siempre el set de datos completo.

4.1 Regresión con Stochastic Gradient Descent

En primer lugar se realiza una regresión con SGD. En esto caso se estudian las métricas de explained variance, negative mean absolute error, negative mean squared error, negative mean squared log error, negative median absolute error, y "r2". Al igual que en el caso del clasificador, se realiza cross validación diviendo el set de datos en 10 partes.

In [32]:
from sklearn.linear_model import SGDRegressor
from sklearn.model_selection import cross_validate


regresor1 = SGDRegressor(loss="huber", penalty="elasticnet",max_iter=100, tol=1e-3)

scoring=['explained_variance','max_error','neg_mean_absolute_error','neg_mean_squared_error','neg_mean_squared_log_error','neg_median_absolute_error','r2']

cv_results = cross_validate(regresor1, vectors, data["overall"], cv = 10, n_jobs=10, scoring = scoring, return_train_score= True)

print('explained_variance:', np.mean(cv_results['test_explained_variance']))
print('neg_mean_absolute_error:', np.mean(cv_results['test_neg_mean_absolute_error']))
print('neg_mean_squared_error:', np.mean(cv_results['test_neg_mean_squared_error']))
print('neg_mean_squared_log_error:', np.mean(cv_results['test_neg_mean_squared_log_error']))
print('neg_median_absolute_error:', np.mean(cv_results['test_neg_median_absolute_error']))
print('r2:', np.mean(cv_results['test_r2']))
explained_variance: 0.0032587367099913833
neg_mean_absolute_error: -2.2340491655778756
neg_mean_squared_error: -5.440162861688758
neg_mean_squared_log_error: -0.3819480112518117
neg_median_absolute_error: -2.330229818340139
r2: -12.01154868680969

4.2 Support vector Regressor

In [11]:
from sklearn.model_selection import cross_validate
from sklearn.svm import SVR
regresor2 = SVR(gamma='auto')

scoring=['explained_variance','max_error','neg_mean_absolute_error','neg_mean_squared_error','neg_mean_squared_log_error','neg_median_absolute_error','r2']
cv_results2 = cross_validate(regresor2, vectors, data["overall"], cv = 10, n_jobs=10, scoring = scoring, return_train_score= True)

print('explained_variance:', np.mean(cv_results2['test_explained_variance']))
print('neg_mean_absolute_error:', np.mean(cv_results2['test_neg_mean_absolute_error']))
print('neg_mean_squared_error:', np.mean(cv_results2['test_neg_mean_squared_error']))
print('neg_mean_squared_log_error:', np.mean(cv_results2['test_neg_mean_squared_log_error']))
print('neg_median_absolute_error:', np.mean(cv_results2['test_neg_median_absolute_error']))
print('r2:', np.mean(cv_results2['test_r2']))
explained_variance: 1.344079914478158e-07
neg_mean_absolute_error: -0.52999794165343
neg_mean_squared_error: -0.5194849391575522
neg_mean_squared_log_error: -0.028310362473006546
neg_median_absolute_error: -0.4799999498540841
r2: -0.13857031884733514

4.3 Nuevo enfoque, Gradient Boosting Regressor con intervalos

Puesto que los resultados obtenidos en los dos casos anteriores fueron malos, se busca un enfoque distinto para poder llevar a cabo la regresión. Si bien el regresor puede estar estimando el valor numérico de forma incorrecta, con un cierto intervalo de confianza podría estar más o menos cerca del valor esperado en función de un cierto margen de error. Se define entonces un nuevo regresor, que esta vez tiene asociados intervalos de confianza para cada dato predicho.

Se aprovecha el uso de quantiles en este regresor para poder definir intervalos de confianza. Así, con 3 regresiones distintas, podemos establecer un rango para el valor predicho y así compararlo con lo esperado.

In [12]:
from sklearn.ensemble import GradientBoostingRegressor

X_train_reg, X_test_reg, y_train_reg, y_test_reg = train_test_split(vectors, data["overall"], test_size = 0.1)

low_coef = 0.1
up_coef  = 0.9

lower_model = GradientBoostingRegressor(loss="quantile",alpha=low_coef)
mid_model = GradientBoostingRegressor(loss="ls")
upper_model = GradientBoostingRegressor(loss="quantile",alpha=up_coef)
In [13]:
lower_model.fit(X_train_reg,y_train_reg)
mid_model.fit(X_train_reg,y_train_reg)
upper_model.fit(X_train_reg,y_train_reg)

predictions_lower = lower_model.predict(X_test_reg)
predictions_mid   = mid_model.predict(X_test_reg)
predictions_upper = upper_model.predict(X_test_reg)
In [52]:
predictions = y_test_reg.tolist()
N_sam = 200
x=np.linspace(1,N_sam,N_sam)

plt.rcParams["figure.figsize"] = (20,10)
plt.plot(x,predictions[0:200],linewidth=5.0)
plt.plot(x,predictions_mid[0:200],linewidth=5.0)
plt.plot(x,predictions_lower[0:200], color='r')
plt.plot(x,predictions_upper[0:200], color='r')
plt.fill_between(x, predictions_lower[0:N_sam],predictions_upper[0:N_sam], color='grey', alpha='0.1')
plt.legend(['Datos', 'predicción','Rango']);
plt.show()

Para este caso particular observadora de 200 muestras, se puede ver que las predicciones en general no aciertan al valor mismo de la calificación. Esto explica para los casos anteriores los valores cercanos a cero de la varianza y score R2 negativos, ya que los modelos pueden ser arbitrariamente malos. Si bien la mayoría de las calificaciones se encuentran dentro del intervalo, aquellas donde la calificación otorgada es mala se escapa del rango. Algo similar ocurre para las calificaciones más altas. Se puede decir entonces que para las predicciones que se escapan de una tendencia media, el modelo no predice bien el valor.

Conclusiones

Tanto el proceso de clasificación como el de regresión no tuvieron resultados óptimos, las métricas evidencian precisiones de entre 50% y 60%. Se asocia esta diferencia principalmente a lo relativa que pueda resultar una descripción con respecto a la percepción de la persona: Para ejemplificar, si se utiliza el clasificador para una review de la forma:

"It was excellent, I loved it"

El clasificador sabe sin problemas que la percepción es buena. Pero en cosas más complejas donde un usuario describe por ejemplo un sabor

"...I tasted some chocolate..."

La calificación otorgada es muy variable, pues depende de si a la persona le gusta o no el chocolate; así, se pueden encontrar reviews similares con calificaciones otorgadas muy distintas.

Si bien depende de casos de estudio particulares, se puede concluir que en general, en set de datos con estas características, las métricas obtenidas no sean las óptimas (i.e. precisión por sobre 90%, por ejemplo).